<?php

namespace StudyBuddy\Modules\Linkedin;

/**
 * Message about debugging/logging:
 * 
 * You may define your own 'd' and 'e' functions
 * (these stand for 'debug' and 'error' logging)
 * prior to using this class
 * the 'd' function should implement the basic
 * logging functionality to log debugging messages
 * to some type of log (usually file but may be to db or to firebig)
 * 
 * Your own 'd' function should (not required but really should)
 * have a method to automatically get the file and line number of calling object/method
 * This is usually acheived by using something like this:
 * $arrBacktrace = debug_backtrace(false);
 * then parsing the $arrBacktrace to extract the element of the calling
 * object/method
 * 
 * If you don't have your own 'd' function then a dummy function
 * is defined here and it does not do anything at all but at least
 * you will not get any errors
 */
if (!function_exists('d')) {

    function d() {
        
    }

}

if (!function_exists('e')) {

    function e() {
        
    }

}

/**
 * Main class for Writing (posting) to LinkedIn OAuth API
 *
 * Instead of defining custom exception classes the
 * object of this class throws specific SPL Exceptions
 * You application should be expecting these types of
 * exceptions:
 *
 * \UnexpectedValueException if user's token, secret were
 * not set prior to making API calls that expect these values!
 *
 * \InvalidArgumentException if invalid value(s) or params
 * are passed to methods
 *
 * \BadMethodCallException if authentication fails (401 error from API)
 * this usually means that user's token and secret are invalid. Maybe user
 * no longer authorized our application or user was banned
 * or deleted by LinkedIn. If this type of exception is caught you should
 * probably mark that user as "no longer connected to LinkedIN"
 * normally you should just delete user's LinkedIn oauth token and secret
 * in your database so that no further attemps are made to use this user
 * in more API calls. You can also show message to user or email that user
 * about this problem.
 *
 * \RuntimeException when something went wrong during API call,
 * including when oauth extension is not enabled on the server
 *
 * @todo feel free to contribute by adding method
 * for JobSearch, JobLoockup, PeopleSearch, CompanySearch API calls
 *
 */
class ApiClient {
    const PROFILE_URL = 'http://api.linkedin.com/v1/people/~:(id,first-name,last-name,industry,picture-url,public-profile-url,location,summary,interests,date-of-birth,twitter-accounts,phone-numbers,skills,im-accounts,educations,certifications,languages)';


    /**
     * @see http://developer.linkedin.com/docs/DOC-1212
     *
     * @var string
     */
    const API_SHARE = 'http://api.linkedin.com/v1/people/~/shares';

    const TPL_SHARE = '<?xml version="1.0" encoding="UTF-8"?>
<share>
  <comment>%1$s</comment>
  <content>
     <title>%2$s</title>
     <submitted-url>%3$s</submitted-url>
     %4$s
  </content>
  <visibility>
     <code>anyone</code>
  </visibility>
</share>';

    /**
     *
     * OAuth
     * @var object of type OAuth
     */
    protected $oAuth;

    /**
     * Use Oauth Token
     * We get this when user signsin with LinkedIn
     * OR when user adds LinkedIn to their account
     * We then get the token, secret from LinkedIn API
     * and store them in the database
     *
     * @var string
     */
    protected $token;

    /**
     * User Oauth Secret
     *
     * Use Oauth Token
     * We get this when user signsin with LinkedIn
     * OR when user adds LinkedIn to their account
     * We then get the token, secret from LinkedIn API
     * and store them in the database
     *
     * @var string
     */
    protected $secret;

    /**
     * Additional request headers
     *
     * @var array
     */
    protected $aHeaders = null;

    /**
     * Array of valid values of Auth types
     * They correspond to constants defined
     * by the oAuth extension
     *
     * @see http://us3.php.net/manual/en/oauth.setauthtype.php
     *
     * @var array
     */
    protected $aValidAuthTypes = array(
        'OAUTH_AUTH_TYPE_AUTHORIZATION',
        'OAUTH_AUTH_TYPE_FORM',
        'OAUTH_AUTH_TYPE_URI',
        'OAUTH_AUTH_TYPE_NONE');

    /**
     * This auth type will be used for the
     * current API call.
     *
     * Auth type indicated how (where) the token, secret
     * params are passed to API: header, query string, form
     * (one NONE)
     *
     * Defaults to OAUTH_AUTH_TYPE_URI meaning that token and secret
     * are appended to the url as query string params
     *
     *
     * @var string
     */
    protected $sAuthType = 'OAUTH_AUTH_TYPE_URI';

    /**
     * Constructor
     *
     * You must register your APP with LinkedIn to get
     * the values of $key and $secret for your app
     * Go here to get yours: here: https://www.linkedin.com/secure/developer
     *
     * @param string $key API KEY. You get this after you register
     * your APP with LinkedIn API.
     *
     * @param string $secret APP OAUTH_SECRET You get this from
     * LinkedIn after registering your APP
     *
     * @throws RuntimeException in case 'oauth' extension
     * is not enabled on the server
     */
    public function __construct($key, $secret) {
        if (!extension_loaded('oauth')) {
            throw new \RuntimeException('Cannot use this class because php extension "oauth" is not loaded');
        }

        $this->oAuth = new \OAuth($key, $secret, OAUTH_SIG_METHOD_HMACSHA1, OAUTH_AUTH_TYPE_URI);
        $this->oAuth->enableDebug();
    }

    /**
     * Set user's oauth token and secret
     *
     * @param string $token user Oauth token
     * @param string $secret user Oauth secret
     *
     * @return object $this
     */
    public function setUserToken($token, $secret) {
        if (!is_string($token)) {
            throw new \InvalidArgumentException('$token param must be a string. Was: ' . gettype($token));
        }

        if (!is_string($secret)) {
            throw new \InvalidArgumentException('$secret param must be a string. Was: ' . gettype($secret));
        }

        $token = \trim($token);
        $secret = \trim($secret);

        if (empty($token) || empty($secret)) {
            throw new \InvalidArgumentException('$token and $secret cannot be empty strings. Was: $token: ' . $secret . ' $secret: ' . $secret);
        }

        $this->token = $token;
        $this->secret = $secret;

        return $this;
    }

    /**
     *
     * Post the update to LinkedIn "Share" API
     *
     * @param string $comment short message in plaintext format
     * this is similar to twitter status message. Maximum 700 chars
     *
     * @param string $label This will become the label of the Link
     *
     * @param string $url the url of the link
     * @param string $image optional = may include the full
     * url of the thumbnail to be used in this update
     *
     * @throws \LogicException if $this->oUser is not set
     * This will be the same when the current user does not
     * have the LinkedIn token/secret credentials.
     *
     * @throws \StudyBuddy\Exception in case the post to LinkedIn API fails
     */
    public function share($comment, $label, $url, $image=null) {

        if (!isset($this->token) || !isset($this->secret)) {
            throw new \UnexpectedValueException('Cannot use API because $this->token and $this->token were not set. Run setUserToken($token, $secret) before using this method');
        }

        $method = 'POST';
        $img = (!empty($image)) ? '<submitted-image-url>' . $image . '</submitted-image-url>' : '';
        $xml = \vsprintf(self::TPL_SHARE, array($comment, $label, $url, $img));
        d('$xml: ' . $xml);

        try {
            /**
             * Setting proper AuthType is very important
             */
            $this->oAuth->setAuthType(OAUTH_AUTH_TYPE_AUTHORIZATION);

            /**
             * Setting proper Content-Type header is extremely important
             * for this API call. It MUST be text/xml
             * and of cause the actual payload must be a valid
             * xml string
             */
            $headers = array('Content-Type' => 'text/xml; charset=UTF-8');

            $this->oAuth->setToken($this->token, $this->secret);
            $this->oAuth->fetch(self::API_SHARE, $xml, $method, $headers);
        } catch (\OAuthException $e) {
            $aDebug = $this->oAuth->getLastResponseInfo();
            d('debug: ' . print_r($aDebug, 1));

            e('OAuthException: ' . $e->getMessage());
            /**
             * Should NOT throw Exception because
             * we are not sure it was actually due to authorization
             * or maby Tumblr was bugged down or something else
             */
            throw new \RuntimeException('Something went wrong during connection with Linkedin. Please try again later' . $e->getMessage());
        }

        return $this->getResponse();
    }

    /**
     * Set the header value
     * This header/value will be used
     * in the next (and all future) requests
     * to the API
     *
     * Example: $this->setHeader('x-li-format', 'json');
     *
     * @param string $header
     * @param string $value
     */
    public function setHeader($header, $value) {
        if (!is_array($this->aHeaders)) {
            $this->aHeaders = array();
        }

        $this->aHeaders[$header] = $value;

        return $this;
    }

    /**
     * Set the format in which we want
     * to receive the data from the LinkedIn API
     *
     * @param string $f must be one of the following:
     * 'xml', 'json', 'jsonp'
     * Usually we don't have much use for jsonp since this
     * requests are for server-side processing
     *
     * @throws \InvalidArgumentException if $f is not a valid
     * format
     *
     * @return object $this
     */
    public function setRequestFormat($f = 'xml') {
        $a = array('xml', 'json', 'jsonp');
        if (!\is_string($f) || empty($f) || !\in_array($f, $a)) {
            throw new \InvalidArgumentException('Param $f must be a non-empty string with one of these values: ' . implode(', ', $a) . ' Was: ' . var_export($f, true));
        }

        $this->setHeader('x-li-format', $f);

        return $this;
    }

    /**
     * @todo unfinished
     * for some reason it is not working - always
     * getting 404 resp
     * while exact same call from Loginlinkedin controller
     * always succeeds just fine.
     *
     *
     * @param string $format data format if which
     * you want to receive response. xml (default) or
     * json
     */
    public function getProfile($format = 'xml') {

        $this->setRequestFormat($format);
        $this->oAuth->setAuthType(\constant('OAUTH_AUTH_TYPE_URI'));

        return $this->fetch(self::PROFILE_URL, true);
    }

    /**
     * Set the way OAuth authorization is passed
     * to LinkedIn API
     *
     *
     * @param string
     * @throws \InvalidArgumentException if $type is not a non-empty string
     *
     * @return object $this
     */
    public function setAuthType($type) {
        if (!\is_string($type) || empty($type)) {
            throw new \InvalidArgumentException('param $type must be a non-empty string. Was: ' . gettype($type));
        }

        if (!\in_array($type, $this->aValidAuthTypes)) {
            throw new \InvalidArgumentException('Param $type is not one of the valid OAUTH Authorization types. Valid values are: ' . implode(', ', $this->aValidAuthTypes) . ' Supplied $type param value was: ' . $type);
        }

        $this->sAuthType = $type;

        return $this;
    }

    /**
     * Returns the AuthType that is
     * currently set
     * This method can be used for debugging when we need
     * to examine what auth type was used for the
     * API call (or about to be used for next API call)
     *
     * @return string
     */
    public function getAuthType() {
        return $this->sAuthType;
    }

    /**
     * Run the fetch() from the LinkedIn API
     *
     *
     * @param string $url url to fetch from
     * This must be valid API endpoint on LinkedIn API
     *
     * @param bool $requireToken if set to true
     * then will verify that $this->token and $this->secret
     * has been set prior to calling this method
     *
     * @throws \UnexpectedValueException if $this->token
     * and $this->secret has not yet been set
     *
     * @return mixed false | string - the response returned from API
     */
    protected function fetch($url, $requireToken = false) {

        $ret = false;

        if (!\is_string($url) || empty($url)) {
            throw new \InvalidArgumentException('param $url must be non-empty string!. Was: ' . gettype($url));
        }

        if ($requireToken) {
            $this->setToken();
        }

        try {
            $this->oAuth->fetch(self::PROFILE_URL, null, 'GET', $this->aHeaders);
            $ret = $this->oAuth->getLastResponse();
        } catch (\Exception $e) {
            $aDebug = $this->oAuth->getLastResponseInfo();
            d('debug: ' . print_r($aDebug, 1));
        }

        return $ret;
    }

    /**
     * Verifies that $this->token and $this->secret
     * has been set
     *
     * @throws \UnexpectedValueException if token and secret
     * have not yet been set
     *
     * @return object $this
     */
    protected function setToken() {
        if (empty($this->token) || empty($this->secret)) {
            throw new \UnexpectedValueException('$this->token AND $this->secret must be set prior to calling fetch(). Set them via setUserToken() method!');
        }

        $this->oAuth->setToken($this->token, $this->secret);

        return $this;
    }

    /**
     *
     * Extract response from Oauth, examine
     * the http response code
     * In case of 401 code - revoke user's LinkedIn Oauth credentials
     *
     * @throws \StudyBuddy\DevException
     */
    protected function getResponse() {
        $ret = $this->oAuth->getLastResponse();

        $aDebug = $this->oAuth->getLastResponseInfo();
        d('debug: ' . print_r($aDebug, 1) . ' ret: ' . $ret);
        if ('200' == $aDebug['http_code'] || '201' == $aDebug['http_code']) {
            d('successful post to API');

            return $ret;
        } elseif ('401' == $aDebug['http_code']) {
            d('Linkedin oauth failed with 401 http code. Data: ' . print_r($aDebug, 1));

            /**
             * This exception should be caught all the way in WebPage and it will
             * cause the ajax message with special key=>value which will
             * trigger the popup to be shown to user with link
             * to signing with LinkedIn
             *
             * Also upon catching this exception we should
             * run revokeLinkedinAuth on the user using the LinkedIn API
             */
            throw new \BadMethodCallException('Linkedin API OAuth credentials failed. Possibly user removed our app');
        } else {
            e('Linkedin API Post failed http code was: ' . $aDebug['http_code'] . ' full debug: ' . print_r($aDebug, 1) . ' response: ' . $ret);

            throw new \RuntimeException('Linkedin OAuth post failed');
        }
    }

}
